# -*- coding: utf-8 -*-

class URL:
    
    def __init__(self, urlstring='', protocol='file',
                 domain='localhost', port=80, path=[''],
                 fragment=['#', ''], params={}):
        self.protocol = protocol
        self.domain = domain
        self.port = port
        self.path = path
        self.fragment = fragment
        self.params = params
        if urlstring:
            self.feed(urlstring)
            
    def feed(self, strurl):
        '''
        Creates an object containing all of the URL's
        properties as attributes : {protocol='', domain='', port=int(),
        path=[], fragment=['#'|'|', ''], params={}}.
        '''
        # The current parsing step. The first step is ALWAYS the
        # protocol.
        # A URL which doesn't show the protocol is therefore invalid.
        step = 0
        # The next step to perform by delimiters. Used to replace long
        # series of conditionnal tests.
        nextstep = {'/':1,
                    '|':2,
                    '#':2,
                    '?':3}
        # The delimiters. Used to simplify the passage to the next
        # step.
        delimiters = ('/', '|', '#', '?')
        # The current data, to be put in modurl after each step, at
        # the appropriate place.
        curdata = []
        # The maximum index possible in the URL string. Used to end
        # a step when the end of the string is met.
        maxindex = len(strurl) - 1
        for index, char in enumerate(strurl):
            if step == 0:
                # Find the URL's protocol.
                if index == maxindex:
                    # Save the last character and stores curdata into
                    # modurl when the end of the string is met.
                    curdata.append(char)
                    self.protocol = ''.join(curdata)
                elif strurl[index:index+3] == '://':
                    # Once the end of the protocol section has been
                    # reached, store curdata into modurl and go to the
                    # next step: finding the domain.
                    self.protocol = ''.join(curdata)
                    curdata = []
                    step = 4
                else:
                    # Join every character until the end of the protocol
                    # section to curdata.
                    curdata.append(char)
            elif step == 4:
                # Find the URL's domain.
                if index == maxindex:
                    # Save the last character when the end of the string
                    # is met.
                    if char not in delimiters:
                        curdata.append(char)
                    # Split the domain into domain and port
                    splitted = ''.join(curdata).split(':', 1)
                    # Store the domain in modurl
                    self.domain = splitted[0]
                    # If there is a port, store it in modurl
                    # TODO: This way of working does not support IPv6,
                    #       where ':' is used as a delimiter inside the
                    #       domain.
                    if len(splitted) == 2:
                        self.port = int(splitted[1])
                elif char in delimiters:
                    # If the current character is a delimiter:
                    if '://' in strurl[:index]:
                        # If the double-slash has been passed (since the
                        # slash is a delimiter):
                        # Split domain into domain and port
                        splitted = ''.join(curdata).split(':', 1)
                        # Store the domain in modurl
                        self.domain = splitted[0]
                        # If there is a port, store it in modurl
                        # TODO: ... (IPv6 support)
                        if len(splitted) == 2:
                            self.port = splitted[1]
                        # Choose the next step by the met delimiter.
                        step = nextstep[char]
                        curdata = []
                        # If the exclusive fragment identifier is met,
                        # modify modurl in consequence.
                        if char == '|':
                            self.fragment[0] = char
                else:
                    curdata.append(char)
            elif step == 1:
                if index == maxindex:
                    curdata.append(char)
                    # Split the path into list items (folders and
                    # filename).
                    self.path = ''.join(curdata).split('/')
                elif char in delimiters[1:]:
                    # Split the path into list items (folders and
                    # filename).
                    self.path = ''.join(curdata).split('/')
                    curdata = []
                    step = nextstep[char]
                    if char == '|':
                        self.fragment[0] = char
                else:
                    curdata.append(char)
            elif step == 2:
                if index == maxindex:
                    curdata.append(char)
                    self.fragment = ''.join(curdata)
                elif char in delimiters[2:]:
                    # Set the content part of the fragment item to
                    # curdata in modurl.
                    self.fragment[1] = ''.join(curdata)
                    curdata = []
                    step = nextstep[char]
                else:
                    curdata.append(char)
            else:
                # The only possible other value for step is 'params'
                if index == maxindex:
                    curdata.append(char)
                    # Make a generator of (name, value) pairs to build a
                    # dict.
                    gen = (var.split('=') for var in \
                           ''.join(curdata).split('&'))
                    # Store the generator build dict in modurl.
                    self.params = dict(gen)
                elif char in delimiters[1:3]:
                    # Parameters are always at the end of the URL
                    curdata.append(char)
                    # Make a generator of (name, value) pairs to build a
                    # dict.
                    gen = (var.split('=') for var in \
                           ''.join(curdata).split('&'))
                    # Store the generator build dict in modurl.
                    self.params = dict(gen)
                    curdata = []
                    step = nextstep['char']
                else:
                    curdata.append(char)
                    
    def __iter__(self):
        return self
    
    def __next__(self):
        for attr in ('protocol', 'domain', 'path',
                     'fragment', 'params'):
            yield getattr(self, attr)

    def fetch(self):
        return fetch(self)

class IncompleteRequest(Exception):
    pass

class IncompleteChunkedRequest(IncompleteRequest):
    pass

class ContinueRequest(Exception):
    pass

class Redirect(Exception):
    pass

class Request:
    
    def __init__(self, source=b'', url=URL(), method=b'HEAD',
                 version=b'HTTP/1.1', content=b'', headers={}, stream=False):
        self.url = url
        self.method = method
        self.version = version
        self.content = content
        self.headers = headers
        self.source = source
        self.stream = stream
        self.step = 0
        self.curdata = b''
        self.chunked = False
        self.contentlen = 0
        self.newstep = False
        self.allchunks = False
        if source:
            self.feed(source)
            
    def feed(self, source, last=False):
        for char in (bytes(i) for i in source):
            print('step', step)
            if self.step == 0:
                # Get method
                if char == b' ':
                    self.method = self.curdata
                    self.curdata = b''
                    self.step = 1
                else:
                    self.curdata += char
            elif self.step == 1:
                # Get path
                if char == b' ':
                    self.version = self.curdata
                    self.curdata = b''
                    self.step = 2
                else:
                    self.curdata += char
            elif self.step == 2:
                # Get version
                if char == b'\r':
                    self.version = self.curdata
                    self.curdata = b''
                elif char == b'\n':
                    self.step = 3
                else:
                    self.curdata += char
            elif self.step == 3:
                # Get headers name
                if char == b'\r' and not self.curdata:
                    continue
                elif char == b'\n' and not self.curdata:
                    if self.chunked:
                        self.step = 6
                    else:
                        self.step = 5
                elif char == b':':
                    self.curdata = [self.curdata, b'']
                    self.step = 4
                else:
                    self.curdata += char
            elif self.step == 4:
                # Get headers value
                if char == b'\r':
                    header = self.curdata[0].strip()
                    value = self.curdata[1].strip()
                    if header in self.headers:
                        self.headers[header].append(value)
                    else:
                        self.headers[header] = [value]
                    self.curdata = b''
                    if (header, value) ==\
                       (b'Transfer-Encoding', b'chunked'):
                        self.chunked = True
                elif char == b'\n':
                    self.step = 3
                else:
                    self.curdata[1] += char
            elif self.step == 5:
                # Get content
                if self.breakindex == 4:
                    last = True
                    self.content = self.content[:-2]
                    break
                elif char == b'\r' and not self.curdata:
                    self.breakindex += 1
                    self.content += char
                elif char == b'\n' and not self.curdata:
                    self.breakindex += 1
                    self.content += char
                else:
                    self.breakindex = 0
                    self.content += char
            elif self.step == 6:
                if char == b'\r' and not self.curdata:
                    break
                elif char in [b';', b'\r']:
                    self.newstep = True
                    chunklen = int(self.curdata, 16)
                    if chunklen:
                        self.contentlen += chunklen
                    else:
                        self.allchunks = True
                        self.step = 3
                    self.curdata = b''
                elif char == b'\n' and self.newstep:
                    self.newstep = False
                    self.step = 7
                else:
                    self.curdata += char
            elif self.step == 7:
                if char == b'\r':
                    self.content += self.curdata
                    self.newstep = True
                elif char == b'\n' and self.newstep:
                    self.newstep = False
                    self.curdata = b''
                    self.step = 6
                else:
                    self.curdata += char
        if (last or not self.stream) and self.chunked and not self.allchunks:
            raise IncompleteChunkedRequest
        elif (last or not self.stream) and not self.chunked and\
             int(self.headers.get(b'Content-Length', [0])[0]) !=\
             len(self.content):
                raise IncompleteRequest
        if last or not self.stream:
            self.step = 0
            self.curdata = b''
            self.chunked = False
            self.contentlen = 0
            self.newstep = False
            self.allchunks = False
            self.stream = False
        if self.allchunks or last:
            return False
        else:
            return True
    
    def __str__(self):
        self.headers[b'Host'] = [bytes('{}:{}'.format(self.url.domain,
                                                      self.url.port),
                                       'utf-8')]
        contentlength = len(self.content)
        if contentlength:
            self.headers[b'Content-Length'] = [contentlength]
        elif b'Content-Length' in self.headers:
            del self.headers[b'Content-Length']
        headersstring = b''
        if self.headers:
            for name, values in self.headers.items():
                for value in values:
                    headersstring += name + b': ' + value + b'\r\n'
        pathstring = b'/' + b'/'.join(bytes(i, 'utf-8') for i in self.url.path)
        request_string = self.method + b' ' + pathstring + b' ' + \
                         self.version + b'\r\n' + headersstring + \
                         b'\r\n' + self.content + b'\r\n\r\n'
        encoding = 'utf-8'
        if b'Content-Type' in self.headers:
            if b';' in self.headers[b'Content-Type']:
                encoding = str(self.headers[b'Content-Type'].split('=')[-1],
                               'utf-8')
        return str(request_string, encoding)

    def __iter__(self):
        self.index = -1
        return self

    def __next__(self):
        string = str(self)
        self.index += 1
        if self.index < len(string):
            return ord(string[self.index])
        else:
            raise StopIteration

class Response:
    def __init__(self, source=b'', version=b'HTTP/1.1',
                 status=[100, b'Continue'], headers={}, content=b'',
                 stream=False):
        self.version = version
        self.status = status
        self.headers = headers
        self.content = content
        self.step = 0
        self.curdata = b''
        self.chunked = False
        self.contentlen = 0
        self.newstep = False
        self.allchunks = False
        self.stream = stream
        self.feed(source)
            
    def feed(self, source, last=False):
        for char in (bytes(i) for i in source):
            if self.step == 0:
                # Get version
                if char == b' ':
                    self.version = self.curdata
                    self.curdata = b''
                    self.step = 1
                else:
                    self.curdata += char
            elif self.step == 1:
                # Get status code
                if char == b' ':
                    if self.curdata == b'100':
                        raise ContinueRequest
                    self.status[0] = int(self.curdata)
                    self.curdata = b''
                    self.step = 2
                else:
                    self.curdata += char
            elif self.step == 2:
                # Get status message
                if char == b'\r':
                    self.status[1] = self.curdata
                    self.curdata = b''
                elif char == b'\n':
                    self.step = 3
                else:
                    self.curdata += char
            elif self.step == 3:
                # Get headers name
                if char == b'\r' and not self.curdata:
                    continue
                elif char == b'\n' and not self.curdata:
                    if self.chunked:
                        self.step = 6
                    else:
                        self.step = 5
                elif char == b':':
                    self.curdata = [self.curdata, b'']
                    self.step = 4
                else:
                    self.curdata += char
            elif self.step == 4:
                # Get headers value
                if char == b'\r':
                    header = self.curdata[0].strip()
                    value = self.curdata[1].strip()
                    if header in self.headers:
                        self.headers[header].append(value)
                    else:
                        self.headers[header] = [value]
                    self.curdata = b''
                    if header == b'Location' and\
                       self.status[0] in range(300, 400):
                        raise Redirect(value)
                    if (header, value) ==\
                       (b'Transfer-Encoding', b'chunked'):
                        self.chunked = True
                elif char == b'\n':
                    self.step = 3
                else:
                    self.curdata[1] += char
            elif self.step == 5:
                # Get content
                if b'Content-Length' in self.headers:
                    if len(self.content) ==\
                       int(self.headers[b'Content-Length']):
                        last = True
                        break
                self.content += char
            elif self.step == 6:
                if char == b'\r' and not self.curdata:
                    break
                elif char in [b';', b'\r']:
                    self.newstep = True
                    chunklen = int(self.curdata, 16)
                    if chunklen:
                        self.contentlen += chunklen
                    else:
                        self.allchunks = True
                        self.step = 3
                    self.curdata = b''
                elif char == b'\n' and self.newstep:
                    self.newstep = False
                    self.step = 7
                else:
                    self.curdata += char
            elif self.step == 7:
                if char == b'\r':
                    self.content += self.curdata
                    self.newstep = True
                elif char == b'\n' and self.newstep:
                    self.newstep = False
                    self.curdata = b''
                    self.step = 6
                else:
                    self.curdata += char
        if (last or not self.stream) and self.chunked and not self.allchunks:
            raise IncompleteChunkedRequest
        elif (last or not self.stream) and not self.chunked and\
             int(self.headers.get(b'Content-Length', [0])[0]) !=\
             len(self.content):
                raise IncompleteRequest
        if last or not self.stream:
            self.step = 0
            self.curdata = b''
            self.chunked = False
            self.contentlen = 0
            self.newstep = False
            self.allchunks = False
            self.stream = False
        if self.allchunks or last:
            return False
        else:
            return True
                    
    def __str__(self):
        contentlength = len(self.content)
        if contentlength:
            self.headers[b'Content-Length'] = [contentlength]
        elif b'Content-Length' in self.headers:
            del self.headers[b'Content-Length']
        headersstring = b''
        if self.headers:
            for name, values in self.headers.items():
                for value in values:
                    headersstring += name + b': ' + value + b'\r\n'
        statusstring = bytes(str(self.status[0]), 'utf-8') + b' ' +\
                       self.status[1]
        request_string = self.version + b' ' + statusstring + b' ' + \
                         b'\r\n' + headersstring + \
                         b'\r\n' + self.content + b'\r\n\r\n'
        encoding = 'utf-8'
        if b'Content-Type' in self.headers:
            if b';' in self.headers[b'Content-Type']:
                encoding = str(self.headers[b'Content-Type'].split('=')[-1],
                               'utf-8')
        return str(request_string, encoding)

def fetch(url=URL):
    pass
